在 Java 5 之前,Java 程序是靠 synchronized 关键字实现锁的功能的,在 Java 5 之后并发包中提供了 Lock 接口及相关实现类(ReentrantLock、CountDownLatch …)来实现锁的功能,而这些实现类内部正是用到了 AbstractQueuedSynchronizer 来实现对应的功能。
简介
队列同步器 AbstractQueuedSynchronizer(简称同步器)是锁和其他同步组件的基础框架,内部使用了一个 int 类型的成员变量来表示同步状态,还使用了一个 FIFO 队列来管理线程的排队工作。
同步状态
同步器内部提供了对同步状态操作的方法,包括设置和获取:
1 | // 同步状态 |
同步器中还有几个空方法,在自定义同步器时可以按需重写,当需要操作同步状态时可通过上面三个方法来完成:
1 | // 独占式获取同步状态 |
此外,同步器还提供了以下常用的模板方法:
- acquire(int arg):独占式获取同步状态,内部是通过
tryAcquire(int arg)
方法实现的。- acquireInterruptibly(int arg):与
acquire(int arg)
相同,但是该方法可以响应中断。- tryAcquireNanos(int arg, long nanosTimeout):在
acquireInterruptibly(int arg)
方法的基础上增加了超时功能。- acquireShared(int arg):共享式获取同步状态,内部是通过
tryAcquireShared(int arg)
方法实现的。- acquireSharedInterruptibly(int arg):与
acquireShared(int arg)
方法相同,但是该方法可以响应中断。- tryAcquireSharedNanos(int arg, long nanosTimeout):在
acquireSharedInterruptibly(int arg)
方法的基础上增加了超时功能。- release(int arg):独占式释放同步状态,内部是通过
tryRelease(int arg)
方法实现的。- releaseShared(int arg):共享式释放同步状态,内部是通过
tryReleaseShared(int arg)
方法实现的。
同步队列
同步器内部通过 FIFO 队列(双向链表)来管理那些获取同步状态失败的线程。当线程获取同步状态失败后,会将当前线程以及一些状态信息构造成一个节点(Node)添加到队列的尾部,同时阻塞该线程;当同步状态被释放后,会把后继节点的线程唤醒并尝试获取同步状态。
Node 是 AQS 的静态内部类:
1 | static final class Node { |
节点(Node)是构成同步队列的基础,同步器拥有头节点和尾节点的引用。获取同步状态失败的线程会构建成节点被添加到同步队列的尾部。
同步队列示意图:
当一个线程获取同步状态成功后,其他线程则无法获取同步状态,转而被构造成节点添加到同步队列的尾部。这个添加过程必须是线程安全的,所以同步器提供了一个基于 CAS 的方法 compareAndSetTail(Node expect, Node update)
来完成添加。
头节点在释放同步状态后会通知后继节点,当后继节点获取同步状态成功后将自己设置为头节点。同步器同样提供了一个基于 CAS 的方法 compareAndSetHead(Node update)
。
实现分析
下面将从独占式获取与释放同步状态、共享式获取与释放同步状态来分析同步器的实现。
独占式获取与释放同步状态
首先看一个自定义独占式同步器用法的示例:
1 | // 这是一个独占锁 |
Mutex 是一个独占锁,在同一个时刻只允许一个线程占有锁,Sync 是个继承了 AbstractQueuedSynchronizer 的静态内部类,重写了同步器的空方法并实现了具体的逻辑,这种方式是官方所推荐的。
独占式获取同步状态
调用同步器的 acquire(int arg)
方法可以获取同步状态,该方法不响应中断操作。也就是说当线程获取同步状态失败后会加入到同步队列中,如果此时对线程进行中断操作,线程不会从同步队列中移出。
下面看看 acquire(int arg)
方法的实现:
1 | public final void acquire(int arg) { |
acquire(int arg)
方法的代码虽然少,但是做的事却不少,接下来分几步进行介绍。
获取同步状态
这一步是通过 tryAcquire(arg)
方法来完成的,而这个方法是需要重写的。
构建节点
如果获取同步状态失败,会用当前线程和其他状态信息构建一个节点:
1 | private Node addWaiter(Node mode) { |
添加到同步队列
添加节点到同步队列,这里有两种情况:一种是同步队列为空的时候,也就是说当前线程是第二个获取同步状态的,此时还没有头节点和尾节点,然后添加了一个空节点(new Node()
);另一种情况是同步队列不为空的时候:
1 | private Node enq(final Node node) { |
自旋
这个过程其实就是当前节点在死循环中获取同步状态:
1 | final boolean acquireQueued(final Node node, int arg) { |
独占式的获取同步状态经过前面四步就完成了,画个流程图加深下印象:
在获取同步状态时,同步器维护了一个同步队列,获取状态失败的线程都会被构建成节点加入到队列中并进行自旋;移出队列的条件是前驱节点为头节点且成功获取了同步状态。
独占式释放同步状态
线程获取同步状态成功并执行完相关逻辑后就要释放同步状态,后继节点就可以继续获取同步状态。而调用同步器的 release(int arg)
方法可以释放同步状态:
1 | public final boolean release(int arg) { |
所以释放同步状态就是修改同步状态,并且唤醒后继节点的线程。
共享式获取与释放同步状态
共享式与独占式最大的区别在于:同一个时刻能否允许多个线程同时获取到同步状态。
再来看自定义的共享式同步器的示例:
1 | class BooleanLatch { |
BooleanLatch 是一个共享锁,在同一个时刻允许多个线程占有锁,Sync 是个继承了 AbstractQueuedSynchronizer 的静态内部类。
共享式获取同步状态
调用同步器的 acquireShared(int arg)
方法可以获取同步状态。下面看看代码实现:
1 | public final void acquireShared(int arg) { |
在 acquireShared()
方法中尝试调用 tryAcquireShared()
方法获取同步状态,当 tryAcquireShared()
的返回值大于等于 0 表示获取同步状态成功。doAcquireShared()
方法表示自旋,跳出循环的条件是前驱节点是头节点并且 tryAcquireShared()
返回值大于等于 0。
共享式释放同步状态
和独占式一样,共享式也需要释放同步状态,通过调用同步器的 releaseShared(int arg)
方法释放同步状态:
1 | public final boolean releaseShared(int arg) { |
在 releaseShared()
方法中尝试调用 tryReleaseShared()
方法释放同步状态,当 tryReleaseShared()
返回 true 表示同步状态已经修改成功。doReleaseShared()
方法主要是修改头节点的等待状态以及唤醒后继节点的线程。
小结
获取同步状态失败后会把当前线程以及其他一些状态信息构建成节点添加到同步队列中(如果此时同步队列是空的,那么会先添加一个空节点,然后再添加这个节点),如果是独占式获取,新构建的节点是 Node.EXCLUSIVE
,否则是 Node.SHARED
,加入到同步队列后节点会自旋(死循环获取同步状态)。
释放同步状态成功后,将会唤醒后继节点的线程,后继节点会在自旋状态中获取到同步状态,然后从同步队列中移除。
参考
《Java 并发编程的艺术–方腾飞、魏鹏、程晓明》